import QuantLib as ql
import pandas as pd
today = ql.Date(28, ql.April, 2023)
ql.Settings.instance().evaluationDate = todayPayment-in-kind bonds
In this kind of bonds, the interest matured during a coupon period is not paid off but added as additional notional. This doesn’t fit very well the currently available coupon types, whose notional is supposed to be known upon construction. However, for fixed-rate PIK bonds we can work around the limitation.
A workaround
The idea is to build the coupons one by one, each time feeding into a coupon information from the previous one. Let’s say we have the usual information for a bond:
start_date = ql.Date(8, ql.February, 2021)
maturity_date = ql.Date(8, ql.February, 2026)
frequency = ql.Semiannual
calendar = ql.TARGET()
convention = ql.Following
settlement_days = 3
coupon_rate = 0.03
day_counter = ql.Thirty360(ql.Thirty360.BondBasis)
face_amount = 10000We first build the schedule, as we would do for a vanilla fixed-rate bond.
schedule = ql.Schedule(
start_date,
maturity_date,
ql.Period(frequency),
calendar,
convention,
convention,
ql.DateGeneration.Backward,
False,
)Instead of using the usual classes or functions for building a bond or a whole fixed-rate leg, though, we’ll start by building only the first coupon:
coupons = []
coupons.append(
ql.FixedRateCoupon(
calendar.adjust(schedule[1]),
face_amount,
coupon_rate,
day_counter,
schedule[0],
schedule[1],
)
)The rest of the coupons can now be built one by one, each time taking the amount from the previous coupon and adding it to the notional:
for i in range(2, len(schedule)):
previous = coupons[-1]
coupons.append(
ql.FixedRateCoupon(
calendar.adjust(schedule[i]),
previous.nominal() + previous.amount(),
coupon_rate,
day_counter,
schedule[i - 1],
schedule[i],
)
)Finally, we can build a Bond instance by passing the list of coupons:
bond = ql.Bond(settlement_days, calendar, start_date, coupons)We can check the resulting cash flows:
def coupon_info(cf):
c = ql.as_coupon(cf)
if not c:
return (cf.date(), None, None, cf.amount())
else:
return (c.date(), c.nominal(), c.rate(), c.amount())
(
pd.DataFrame(
[coupon_info(c) for c in bond.cashflows()],
columns=("date", "nominal", "rate", "amount"),
index=range(1, len(bond.cashflows()) + 1),
).style.format(
{"amount": "{:.2f}", "nominal": "{:.2f}", "rate": "{:.2%}"}
)
)| date | nominal | rate | amount | |
|---|---|---|---|---|
| 1 | August 9th, 2021 | 10000.00 | 3.00% | 150.83 |
| 2 | August 9th, 2021 | nan | nan% | -150.83 |
| 3 | February 8th, 2022 | 10150.83 | 3.00% | 151.42 |
| 4 | February 8th, 2022 | nan | nan% | -151.42 |
| 5 | August 8th, 2022 | 10302.25 | 3.00% | 154.53 |
| 6 | August 8th, 2022 | nan | nan% | -154.53 |
| 7 | February 8th, 2023 | 10456.78 | 3.00% | 156.85 |
| 8 | February 8th, 2023 | nan | nan% | -156.85 |
| 9 | August 8th, 2023 | 10613.64 | 3.00% | 159.20 |
| 10 | August 8th, 2023 | nan | nan% | -159.20 |
| 11 | February 8th, 2024 | 10772.84 | 3.00% | 161.59 |
| 12 | February 8th, 2024 | nan | nan% | -161.59 |
| 13 | August 8th, 2024 | 10934.43 | 3.00% | 164.02 |
| 14 | August 8th, 2024 | nan | nan% | -164.02 |
| 15 | February 10th, 2025 | 11098.45 | 3.00% | 168.33 |
| 16 | February 10th, 2025 | nan | nan% | -168.33 |
| 17 | August 8th, 2025 | 11266.78 | 3.00% | 167.12 |
| 18 | August 8th, 2025 | nan | nan% | -167.12 |
| 19 | February 9th, 2026 | 11433.90 | 3.00% | 172.46 |
| 20 | February 9th, 2026 | nan | nan% | 11433.90 |
For each date before maturity, we see two opposite payments: the 3% interest payment (with a positive sign since it’s made to the holder of the bond) as well as a negative payment that models the same amount being immediately put by the holder into the bond.
We didn’t create those payments explicitly; the Bond constructor created them automatically to account for the change in face amount between consecutive coupons. This feature was coded in order to generate amortizing payments in case of a decreasing face amount, but it works in the opposite direction just as well.
At maturity, we also have two payments; however, this time they are the 3% interest payment and the final reimbursement. Together, they give the final payment:
final_payment = (
bond.cashflows()[-2].amount() + bond.cashflows()[-1].amount()
)
final_payment11606.360684055619
One thing to note, though, is that asking the bond for its price might not give the expected result. Let’s use a null rate for discounting, so we can spot the issue immediately:
discount_curve = ql.FlatForward(today, 0.0, ql.Actual365Fixed())
bond.setPricingEngine(
ql.DiscountingBondEngine(ql.YieldTermStructureHandle(discount_curve))
)
bond.dirtyPrice()109.35330081248894
Apart for the scaling to base 100, you might have expected the (undiscounted) bond value to equal the final amount. However, according to the convention for amortizing bonds, the dirtyPrice method also scales the price with respect to the current notional of the bond:
current_notional = bond.notional(bond.settlementDate())
current_notional10613.635434706595
final_payment * 100 / current_notional109.35330081248891
To avoid rescaling, we can use another method:
bond.settlementValue()11606.36068405562
The NPV method would also work; the difference is that NPV discounts to the reference date of the discount curve (today’s date, in this case) while settlementValue discounts to the settlement date of the bond.
Limitations
Of course, this works without problems if the coupon rate is fixed. For floating-rate bonds, we can use the same workaround; but the resulting cashflows and the bond will need to be thrown away and rebuilt when the forecasting curve changes, because their face amount will also change. This means that we can calculate one-off prices, but we won’t be able to keep the bond and have it react when the curve changes, as vanilla floaters do.